package com.ly.component.threadpool.util;

import com.ly.component.threadpool.callback.CancelRunnable;
import com.ly.component.threadpool.callback.SubmitRunnable;
import lombok.ToString;

import javax.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;

public class DelayScheduleTaskManager {
	private final String scheduleTaskName = String.format("%s[%s]", getClass().getName(), "@Schedule-Task");
	private final Set<String> scheduledName = new CopyOnWriteArraySet<>();
	private SpringTaskManager springTaskManager = SpringTaskManager.getInstance();
	private DelayQueue<Params> delayedQueue = new DelayQueue<>();

	public static DelayScheduleTaskManager getInstance() {
		return Instance.DEFAULT.delayScheduleTaskManager;
	}

	public boolean schedule(
			String name,
			Map<String, Object> params,
			SubmitRunnable submitRunnable,
			CancelRunnable cancelRunnable,
			long delay,
			long period
						   ) {
		Params p = new Params(name, submitRunnable, cancelRunnable, 0L, delay, period, params);
		synchronized (scheduledName) {
			if (!scheduledName.contains(name)) {
				scheduledName.add(name);
				delayedQueue.put(p);
				triggerSchedule();
				return true;
			}
		}
		return false;
	}

	public boolean cancel(String name) {
		synchronized (scheduledName) {
			if (scheduledName.contains(name)) {
				scheduledName.remove(name);
				springTaskManager.cancel(name);
				return true;
			}
		}
		return false;
	}

	private void triggerSchedule() {
		task();
	}

	private void task() {
		springTaskManager.submit(scheduleTaskName, null, params -> {
			while (true) {
				if (!schedule()) {
					break;
				}
			}
			return true;
		}, params -> true);
	}

	private boolean schedule() {
		if (delayedQueue.isEmpty()) {
			return false;
		}
		List<Params> runnableList = getParams();
		if (runnableList == null) {
			return false;
		}
		runnableList.parallelStream()
					.forEach(params -> submitTask(params, params.period > 0));
		return true;
	}

	private void periodParamReset(Params params) {
		params.time = System.currentTimeMillis();
		params.delay = 0L;
		delayedQueue.put(params);
	}

	private void reSubmitParamReset(Params params) {
		params.time = System.currentTimeMillis();
		params.delay = 1 - params.period;
		delayedQueue.put(params);
	}

	private void submitTask(Params params, boolean period) {
		boolean submitFlag = springTaskManager.submit(params.name, params.params, params.submitRunnable, params.cancelRunnable);
		if (submitFlag) {
			if (!period) {
				scheduledName.remove(params.name);
			} else {
				periodParamReset(params);
			}
		} else {
			reSubmitParamReset(params);
		}
	}

	private List<Params> getParams() {
		try {
			Params p = delayedQueue.take();
			List<Params> paramsList = new ArrayList<>();
			while (p != null) {
				synchronized (scheduledName) {
					if (scheduledName.contains(p.name)) {
						paramsList.add(p);
					}
				}
				p = delayedQueue.poll();
			}
			return paramsList;
		} catch (InterruptedException e) {
			return new ArrayList<>();
		} catch (IllegalMonitorStateException e) {
			return null;
		}
	}

	@PreDestroy
	public void destroy() {
		scheduledName.forEach(this::cancel);
		springTaskManager.destroy();
	}

	private enum Instance {
		DEFAULT(new DelayScheduleTaskManager());

		private DelayScheduleTaskManager delayScheduleTaskManager;

		Instance(DelayScheduleTaskManager delayScheduleTaskManager) {
			this.delayScheduleTaskManager = delayScheduleTaskManager;
		}
	}

	@ToString
	private class Params implements Delayed {
		private String name;
		private SubmitRunnable submitRunnable;
		private CancelRunnable cancelRunnable;
		private Long time;
		private Long delay;
		private Long period;
		private Map<String, Object> params;

		public Params(
				String name,
				SubmitRunnable submitRunnable,
				CancelRunnable cancelRunnable,
				Long initialDelay,
				Long delay,
				Long period,
				Map<String, Object> params
					 ) {
			this.time = System.currentTimeMillis();
			this.name = name;
			this.submitRunnable = submitRunnable;
			this.cancelRunnable = cancelRunnable;
			this.delay = initialDelay + delay;
			this.period = period;
			this.params = params;
		}

		@Override
		public long getDelay(TimeUnit unit) {
			return (time + period + delay) - System.currentTimeMillis();
		}

		@Override
		public int compareTo(Delayed o) {
			return (int) (getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS));
		}
	}
}
